{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import cv2\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import math\n",
    "from sklearn.cluster import DBSCAN\n",
    "from sklearn.preprocessing import MinMaxScaler\n",
    "from collections import defaultdict\n",
    "from scipy.linalg import block_diag\n",
    "from __future__ import division"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class LaneTracker:\n",
    "    def __init__(self, n_lanes, proc_noise_scale, meas_noise_scale, process_cov_parallel=0, proc_noise_type='white'):\n",
    "        self.n_lanes = n_lanes\n",
    "        self.meas_size = 4 * self.n_lanes\n",
    "        self.state_size = self.meas_size * 2\n",
    "        self.contr_size = 0\n",
    "\n",
    "        self.kf = cv2.KalmanFilter(self.state_size, self.meas_size, self.contr_size)\n",
    "        self.kf.transitionMatrix = np.eye(self.state_size, dtype=np.float32)\n",
    "        self.kf.measurementMatrix = np.zeros((self.meas_size, self.state_size), np.float32)\n",
    "        for i in range(self.meas_size):\n",
    "            self.kf.measurementMatrix[i, i*2] = 1\n",
    "\n",
    "        if proc_noise_type == 'white':\n",
    "            block = np.matrix([[0.25, 0.5],\n",
    "                               [0.5, 1.]], dtype=np.float32)\n",
    "            self.kf.processNoiseCov = block_diag(*([block] * self.meas_size)) * proc_noise_scale\n",
    "        if proc_noise_type == 'identity':\n",
    "            self.kf.processNoiseCov = np.eye(self.state_size, dtype=np.float32) * proc_noise_scale\n",
    "        for i in range(0, self.meas_size, 2):\n",
    "            for j in range(1, self.n_lanes):\n",
    "                self.kf.processNoiseCov[i, i+(j*8)] = process_cov_parallel\n",
    "                self.kf.processNoiseCov[i+(j*8), i] = process_cov_parallel\n",
    "\n",
    "        self.kf.measurementNoiseCov = np.eye(self.meas_size, dtype=np.float32) * meas_noise_scale\n",
    "\n",
    "        self.kf.errorCovPre = np.eye(self.state_size)\n",
    "\n",
    "        self.meas = np.zeros((self.meas_size, 1), np.float32)\n",
    "        self.state = np.zeros((self.state_size, 1), np.float32)\n",
    "\n",
    "        self.first_detected = False\n",
    "\n",
    "    def _update_dt(self, dt):\n",
    "        for i in range(0, self.state_size, 2):\n",
    "            self.kf.transitionMatrix[i, i+1] = dt\n",
    "\n",
    "    def _first_detect(self, lanes):\n",
    "        for l, i in zip(lanes, range(0, self.state_size, 8)):\n",
    "            self.state[i:i+8:2, 0] = l\n",
    "        self.kf.statePost = self.state\n",
    "        self.first_detected = True\n",
    "\n",
    "    def update(self, lanes):\n",
    "        if self.first_detected:\n",
    "            for l, i in zip(lanes, range(0, self.meas_size, 4)):\n",
    "                if l is not None:\n",
    "                    self.meas[i:i+4, 0] = l\n",
    "            self.kf.correct(self.meas)\n",
    "        else:\n",
    "            if lanes.count(None) == 0:\n",
    "                self._first_detect(lanes)\n",
    "\n",
    "    def predict(self, dt):\n",
    "        if self.first_detected:\n",
    "            self._update_dt(dt)\n",
    "            state = self.kf.predict()\n",
    "            lanes = []\n",
    "            for i in range(0, len(state), 8):\n",
    "                lanes.append((state[i], state[i+2], state[i+4], state[i+6]))\n",
    "            return lanes\n",
    "        else:\n",
    "            return None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class LaneDetector:\n",
    "    def __init__(self, road_horizon, prob_hough=True):\n",
    "        self.prob_hough = prob_hough\n",
    "        self.vote = 50\n",
    "        self.roi_theta = 0.3\n",
    "        self.road_horizon = road_horizon\n",
    "\n",
    "    def _standard_hough(self, img, init_vote):\n",
    "        # Hough transform wrapper to return a list of points like PHough does\n",
    "        lines = cv2.HoughLines(img, 1, np.pi/180, init_vote)\n",
    "        points = [[]]\n",
    "        for l in lines:\n",
    "            for rho, theta in l:\n",
    "                a = np.cos(theta)\n",
    "                b = np.sin(theta)\n",
    "                x0 = a*rho\n",
    "                y0 = b*rho\n",
    "                x1 = int(x0 + 1000*(-b))\n",
    "                y1 = int(y0 + 1000*a)\n",
    "                x2 = int(x0 - 1000*(-b))\n",
    "                y2 = int(y0 - 1000*a)\n",
    "                points[0].append((x1, y1, x2, y2))\n",
    "        return points\n",
    "\n",
    "    def _base_distance(self, x1, y1, x2, y2, width):\n",
    "        # compute the point where the give line crosses the base of the frame\n",
    "        # return distance of that point from center of the frame\n",
    "        if x2 == x1:\n",
    "            return (width*0.5) - x1\n",
    "        m = (y2-y1)/(x2-x1)\n",
    "        c = y1 - m*x1\n",
    "        base_cross = -c/m\n",
    "        return (width*0.5) - base_cross\n",
    "\n",
    "    def _scale_line(self, x1, y1, x2, y2, frame_height):\n",
    "        # scale the farthest point of the segment to be on the drawing horizon\n",
    "        if x1 == x2:\n",
    "            if y1 < y2:\n",
    "                y1 = self.road_horizon\n",
    "                y2 = frame_height\n",
    "                return x1, y1, x2, y2\n",
    "            else:\n",
    "                y2 = self.road_horizon\n",
    "                y1 = frame_height\n",
    "                return x1, y1, x2, y2\n",
    "        if y1 < y2:\n",
    "            m = (y1-y2)/(x1-x2)\n",
    "            x1 = ((self.road_horizon-y1)/m) + x1\n",
    "            y1 = self.road_horizon\n",
    "            x2 = ((frame_height-y2)/m) + x2\n",
    "            y2 = frame_height\n",
    "        else:\n",
    "            m = (y2-y1)/(x2-x1)\n",
    "            x2 = ((self.road_horizon-y2)/m) + x2\n",
    "            y2 = self.road_horizon\n",
    "            x1 = ((frame_height-y1)/m) + x1\n",
    "            y1 = frame_height\n",
    "        return x1, y1, x2, y2\n",
    "\n",
    "    def detect(self, frame):\n",
    "        img = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)\n",
    "\n",
    "        roiy_end = frame.shape[0]\n",
    "        roix_end = frame.shape[1]\n",
    "        roi = img[self.road_horizon:roiy_end, 0:roix_end]\n",
    "        blur = cv2.medianBlur(roi, 5)\n",
    "        contours = cv2.Canny(blur, 60, 120)\n",
    "\n",
    "        if self.prob_hough:\n",
    "            lines = cv2.HoughLinesP(contours, 1, np.pi/180, self.vote, minLineLength=30, maxLineGap=100)\n",
    "        else:\n",
    "            lines = self.standard_hough(contours, self.vote)\n",
    "\n",
    "        if lines is not None:\n",
    "            # find nearest lines to center\n",
    "            lines = lines+np.array([0, self.road_horizon, 0, self.road_horizon]).reshape((1, 1, 4))  # scale points from ROI coordinates to full frame coordinates\n",
    "            left_bound = None\n",
    "            right_bound = None\n",
    "            for l in lines:\n",
    "                # find the rightmost line of the left half of the frame and the leftmost line of the right half\n",
    "                for x1, y1, x2, y2 in l:\n",
    "                    theta = np.abs(np.arctan2((y2-y1), (x2-x1)))  # line angle WRT horizon\n",
    "                    if theta > self.roi_theta:  # ignore lines with a small angle WRT horizon\n",
    "                        dist = self._base_distance(x1, y1, x2, y2, frame.shape[1])\n",
    "                        if left_bound is None and dist < 0:\n",
    "                            left_bound = (x1, y1, x2, y2)\n",
    "                            left_dist = dist\n",
    "                        elif right_bound is None and dist > 0:\n",
    "                            right_bound = (x1, y1, x2, y2)\n",
    "                            right_dist = dist\n",
    "                        elif left_bound is not None and 0 > dist > left_dist:\n",
    "                            left_bound = (x1, y1, x2, y2)\n",
    "                            left_dist = dist\n",
    "                        elif right_bound is not None and 0 < dist < right_dist:\n",
    "                            right_bound = (x1, y1, x2, y2)\n",
    "                            right_dist = dist\n",
    "            if left_bound is not None:\n",
    "                left_bound = self._scale_line(left_bound[0], left_bound[1], left_bound[2], left_bound[3], frame.shape[0])\n",
    "            if right_bound is not None:\n",
    "                right_bound = self._scale_line(right_bound[0], right_bound[1], right_bound[2], right_bound[3], frame.shape[0])\n",
    "\n",
    "            return [left_bound, right_bound]\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def region_of_interest(img, vertices):\n",
    "    # Define a blank matrix that matches the image height/width.\n",
    "    mask = np.zeros_like(img)\n",
    "    \n",
    "    # Retrieve the number of color channels of the image.\n",
    "    #channel_count = img.shape[2]\n",
    "    \n",
    "    # color used to fill polygon\n",
    "    match_mask_color = 255\n",
    "      \n",
    "    # Fill the polygon with white\n",
    "    cv2.fillPoly(mask, vertices, (255,255,255))\n",
    "    \n",
    "    # Returning the image only where mask pixels match\n",
    "    masked_image = cv2.bitwise_and(img, mask)\n",
    "    \n",
    "    return masked_image"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def gamma_correction(RGBimage, correct_param = 0.35,equalizeHist = False):\n",
    "    red = RGBimage[:,:,2]\n",
    "    green = RGBimage[:,:,1]\n",
    "    blue = RGBimage[:,:,0]\n",
    "    \n",
    "    red = red/255.0\n",
    "    red = cv2.pow(red, correct_param)\n",
    "    red = np.uint8(red*255)\n",
    "    if equalizeHist:\n",
    "        red = cv2.equalizeHist(red)\n",
    "    \n",
    "    green = green/255.0\n",
    "    green = cv2.pow(green, correct_param)\n",
    "    green = np.uint8(green*255)\n",
    "    if equalizeHist:\n",
    "        green = cv2.equalizeHist(green)\n",
    "        \n",
    "    \n",
    "    blue = blue/255.0\n",
    "    blue = cv2.pow(blue, correct_param)\n",
    "    blue = np.uint8(blue*255)\n",
    "    if equalizeHist:\n",
    "        blue = cv2.equalizeHist(blue)\n",
    "    \n",
    "\n",
    "    output = cv2.merge((blue,green,red))\n",
    "    return output"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def gamma_correction_auto(RGBimage,equalizeHist = False): #0.35\n",
    "    originalFile = RGBimage.copy()\n",
    "    red = RGBimage[:,:,2]\n",
    "    green = RGBimage[:,:,1]\n",
    "    blue = RGBimage[:,:,0]\n",
    "    \n",
    "    forLuminance = cv2.cvtColor(originalFile,cv2.COLOR_BGR2YUV)\n",
    "    Y = forLuminance[:,:,0]\n",
    "    totalPix = vidsize[0]* vidsize[1]\n",
    "    summ = np.sum(Y[:,:])\n",
    "    Yaverage = np.divide(totalPix,summ)\n",
    "    #Yclipped = np.clip(Yaverage,0,1)\n",
    "    epsilon = 1.19209e-007\n",
    "    correct_param = np.divide(-0.3,np.log10([Yaverage + epsilon]))\n",
    "    correct_param = 0.7 - correct_param \n",
    "\n",
    "    red = red/255.0\n",
    "    red = cv2.pow(red, correct_param)\n",
    "    red = np.uint8(red*255)\n",
    "    if equalizeHist:\n",
    "        red = cv2.equalizeHist(red)\n",
    "    \n",
    "    green = green/255.0\n",
    "    green = cv2.pow(green, correct_param)\n",
    "    green = np.uint8(green*255)\n",
    "    if equalizeHist:\n",
    "        green = cv2.equalizeHist(green)\n",
    "        \n",
    "    \n",
    "    blue = blue/255.0\n",
    "    blue = cv2.pow(blue, correct_param)\n",
    "    blue = np.uint8(blue*255)\n",
    "    if equalizeHist:\n",
    "        blue = cv2.equalizeHist(blue)\n",
    "    \n",
    "\n",
    "    output = cv2.merge((blue,green,red))\n",
    "    #print(correct_param)\n",
    "    return output"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def hsv_filter(image, min_val_y, max_val_y,  min_val_w, max_val_w):\n",
    "    \"\"\"\n",
    "    A function returning a mask for pixels within min_val - max_val range\n",
    "    Inputs:\n",
    "    - image - a BGR image you want to apply function on\n",
    "    - min_val_y - array of shape (3,) giving minumum HSV values for yellow color\n",
    "    - max_val_y - array of shape (3,) giving maximum HSV values for yellow color\n",
    "    - min_val_w - array of shape (3,) giving minumum HSV values for white color\n",
    "    - max_val_w - array of shape (3,) giving maximum HSV values for white color\n",
    "    Returns:\n",
    "    - img_filtered - image of pixels being in given threshold\n",
    "    \"\"\"\n",
    "    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)\n",
    "    mask_yellow = cv2.inRange(hsv, min_val_y, max_val_y)\n",
    "    mask_white = cv2.inRange(hsv, min_val_w, max_val_w)\n",
    "    mask = cv2.bitwise_or(mask_yellow, mask_white)\n",
    "    img_filtered = cv2.bitwise_and(image, image, mask=mask)\n",
    "    \n",
    "    return img_filtered"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def hough_transform(original, gray_img, threshold, discard_horizontal = 0.4):\n",
    "    \"\"\"\n",
    "    A function fitting lines that intersect >=threshold white pixels\n",
    "    Input:\n",
    "    - original - image we want to draw lines on\n",
    "    - gray_img - image with white/black pixels, e.g. a result of Canny Edge Detection\n",
    "    - threshold - if a line intersects more than threshold white pixels, draw it\n",
    "    - discard_horizontal - smallest abs derivative of line that we want to take into account\n",
    "    Return:\n",
    "    - image_lines - result of applying the function\n",
    "    - lines_ok - rho and theta\n",
    "    \"\"\"\n",
    "    lines = cv2.HoughLines(gray_img, 0.5, np.pi / 360, threshold)\n",
    "    image_lines = original\n",
    "    lines_ok = [] #list of parameters of lines that we want to take into account (not horizontal)\n",
    "            \n",
    "    if lines is not None:\n",
    "        for i in range(0, len(lines)):\n",
    "            rho = lines[i][0][0]\n",
    "            theta = lines[i][0][1]\n",
    "            #discard horizontal lines\n",
    "            m = -math.cos(theta)/(math.sin(theta)+1e-10) #adding some small value to avoid dividing by 0\n",
    "            if abs(m) < discard_horizontal:\n",
    "                continue\n",
    "            else:\n",
    "                a = math.cos(theta)\n",
    "                b = math.sin(theta)\n",
    "                x0 = a * rho\n",
    "                y0 = b * rho\n",
    "                pt1 = (int(x0 + 1000*(-b)), int(y0 + 1000*(a)))\n",
    "                pt2 = (int(x0 - 1000*(-b)), int(y0 - 1000*(a)))\n",
    "                cv2.line(image_lines, pt1, pt2, (0,0,255), 2, cv2.LINE_AA)\n",
    "                lines_ok.append([rho,theta])\n",
    "        \n",
    "    lines_ok = np.array(lines_ok)\n",
    "                    \n",
    "    return image_lines, lines_ok"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def clustering(lines, original, region_of_interest_points, eps = 0.05, min_samples = 3):\n",
    "    \"\"\"\n",
    "    A function using DBSCAN clustering algorithm for finding best lines to be drawn on the output video\n",
    "    Inputs:\n",
    "    - lines - output of hough tranform function, array containing parameters of found lines\n",
    "    - original - image we want to draw final lines on\n",
    "    - region_of_interest_points - for drawing lines of desired length\n",
    "    Output:\n",
    "    - img - image with detected lane lines drawn\n",
    "    \"\"\"\n",
    "    img = original\n",
    "    img_lines = np.zeros_like(img, dtype=np.int32)\n",
    "\n",
    "    if lines.shape[0] != 0:\n",
    "        #preprocessing features to be in (0-1) range\n",
    "        scaler = MinMaxScaler()\n",
    "        scaler.fit(lines)\n",
    "        lines = scaler.fit_transform(lines)\n",
    "\n",
    "        db = DBSCAN(eps=eps, min_samples=min_samples).fit(lines) #applying DBSCAN Algorithm on our normalized lines\n",
    "        labels = db.labels_\n",
    "\n",
    "        lines = scaler.inverse_transform(lines) #getting back our original values\n",
    "\n",
    "        grouped = defaultdict(list)\n",
    "        #grouping lines by clusters\n",
    "        for i, label in enumerate(labels):\n",
    "            grouped[label].append([lines[i,0],lines[i,1]])\n",
    "\n",
    "        num_clusters = np.max(labels) + 1\n",
    "        means = []\n",
    "        #getting mean values by cluster\n",
    "        for i in range(num_clusters):\n",
    "            mean = np.mean(np.array(grouped[i]), axis=0)\n",
    "            means.append(mean)\n",
    "\n",
    "        means = np.array(means)\n",
    "        \n",
    "        #printing the result on original image\n",
    "        for rho, theta in means:\n",
    "            a = math.cos(theta)\n",
    "            b = math.sin(theta)\n",
    "            x0 = a * rho\n",
    "            y0 = b * rho\n",
    "            pt1 = (int(x0 + 1000*(-b)), int(y0 + 1000*(a)))\n",
    "            pt2 = (int(x0 - 1000*(-b)), int(y0 - 1000*(a)))\n",
    "            cv2.line(img, pt1, pt2, (255,255,255), 2, cv2.LINE_AA)\n",
    "        \n",
    "    return img"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def IPM(image,ROI_points):\n",
    "    pts_src = np.array(ROI_points,dtype=float)\n",
    "    size = (700,600,3)\n",
    "    ipm_out = np.zeros(size, np.uint8)\n",
    "    \n",
    "    dst_points = np.array(\n",
    "                       [\n",
    "                        [0,0],\n",
    "                        [size[0] - 1, 0],\n",
    "                        [size[0] - 1, size[1] -1],\n",
    "                        [0, size[1] - 1 ]\n",
    "                        ], dtype=float\n",
    "                       )\n",
    "    h, status = cv2.findHomography(pts_src, dst_points)\n",
    "    ipm_out = cv2.warpPerspective(image, h, size[0:2])\n",
    "    ipm_out = cv2.rotate(ipm_out,cv2.ROTATE_90_COUNTERCLOCKWISE)\n",
    "    \n",
    "    return ipm_out"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "collapsed": true,
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "#capturing video\n",
    "cap = cv2.VideoCapture('small_nightRoad.mp4')\n",
    "vidsize = (640,480,3)\n",
    "\n",
    "#out = cv2.VideoWriter('final_kalman_night.avi', -1, 40.0, None, True)\n",
    "\n",
    "#defining corners for ROI\n",
    "height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)\n",
    "width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)\n",
    "\n",
    "topLeftPt = (0, height*(3.1/5))\n",
    "topRightPt = (width, height*(3.1/5))\n",
    "\n",
    "region_of_interest_points = [\n",
    "(0, height),\n",
    "#(0, height*(4.1/5)),\n",
    "topLeftPt,\n",
    "topRightPt,\n",
    "#(width, height*(4.1/5)),\n",
    "(width, height),\n",
    "]\n",
    "\n",
    "#defining color thresholds\n",
    "min_val_y = np.array([15,80,190])\n",
    "max_val_y = np.array([30,255,255])\n",
    "min_val_w = np.array([0,0,195])\n",
    "max_val_w = np.array([255, 80, 255])\n",
    "\n",
    "ticks = 0\n",
    "\n",
    "lt = LaneTracker(2, 0.1, 15)\n",
    "ld = LaneDetector(100)\n",
    "\n",
    "\n",
    "while True:\n",
    "    precTick = ticks\n",
    "    ticks = cv2.getTickCount()\n",
    "    dt = (ticks - precTick) / cv2.getTickFrequency()\n",
    "    ret, frame = cap.read()\n",
    "    if ret:\n",
    "        gamma = gamma_correction_auto(frame,equalizeHist = False) #0.2\n",
    "        #cv2.imshow('gamma', gamma)\n",
    "        cropped = region_of_interest(gamma, np.array([region_of_interest_points], np.int32))\n",
    "        #cv2.imshow('cropped', cropped)\n",
    "        #bilateral = cv2.bilateralFilter(cropped, 9, 80, 80)\n",
    "        #cv2.imshow('bilateral', bilateral)\n",
    "        #hsv = hsv_filter(cropped, min_val_y, max_val_y,  min_val_w, max_val_w)\n",
    "        predicted = lt.predict(dt)\n",
    "\n",
    "        lanes = ld.detect(cropped)\n",
    "                \n",
    "        helper = np.zeros_like(frame)\n",
    "        \n",
    "        if predicted is not None:\n",
    "            cv2.line(helper, (predicted[0][0], predicted[0][1]), (predicted[0][2], predicted[0][3]), (0, 255, 0), 2)\n",
    "            cv2.line(helper, (predicted[1][0], predicted[1][1]), (predicted[1][2], predicted[1][3]), (0, 255, 0), 2)\n",
    "        \n",
    "        helper[:int(helper.shape[0]*0.55),:] = 0\n",
    "        frame = cv2.add(helper,frame)\n",
    "        ipmout = IPM(helper,region_of_interest_points)\n",
    "        lt.update(lanes)\n",
    "        #cv2.imshow('hsv', hsv)\n",
    "        #canny = cv2.Canny(hsv, 80, 255) #100\n",
    "        #cv2.imshow('canny', canny)\n",
    "        #hough, lines = hough_transform(frame, canny, 11, discard_horizontal = 0.7) #14 0.4\n",
    "        #cv2.imshow('hough', hough)\n",
    "        #_, frame = cap.read()\n",
    "        #final = clustering(lines, frame, np.array([region_of_interest_points], np.int32), eps = 0.5, min_samples = 4)\n",
    "        cv2.imshow('final', frame)\n",
    "        cv2.imshow('IPM', ipmout)\n",
    "        #out.write(frame)\n",
    "        \n",
    "        k = cv2.waitKey(1) & 0xFF\n",
    "        if k == ord('q'):\n",
    "            break\n",
    "\n",
    "\n",
    "cap.release()\n",
    "cv2.destroyAllWindows()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
